// created 02/15/2018
// checked 02/15/2018
// tested 7:04, 02/17/2018
// strData.startsWith("Interval?!\r")
package com.walkerma.library;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Locale;
import android.os.Handler;

public class TcpClientServer {
	public static final int MSG_Connected = 10001;
	public static final int MSG_ConnectError = 10002;
	public static final int MSG_Closed = 10003;
	public static final int MSG_DataArrived = 10004;
	public static final int MSG_TransmitError = 10005;
	public static final int MSG_ReadError = 10006;
	public static final int MSG_ReplyOK = 10007;

	public static final int CONNECT_TIMEOUT = 3000;	//ms
	private final int DEFAULT_MIN_DELAY = 50;
	private final int DEFAULT_MAX_DELAY = 10000;
	private int nDelay;

	public enum DataSort{TEXT, BYTES, TEXT_CR, TEXT_CRLF}
	//default data format
	private DataSort nDataSort=DataSort.TEXT_CR; 

	private enum WorkMode{CLIENT, SERVER}
	private WorkMode nWorkMode = WorkMode.CLIENT;

	private Handler mHandler;
	private String strServer;
	private int nPort;

	private ThreadConnect threadConnect;
	private ThreadRead threadRead;

	private ServerSocket serverSocket=null;
	private Socket socket=null;
	private boolean bStopConnect=false;
	private BufferedReader inReader;	//for text
	private BufferedWriter outWriter;
	private BufferedInputStream bis;	// for bytes
	private BufferedOutputStream bos;
	private boolean bConnected;


	private void getSocket() {
		if(nWorkMode==WorkMode.CLIENT){
			socket = new Socket();
			// 07/25/15 connect will be blocked
			try {
				socket.connect(new InetSocketAddress(
						strServer, nPort), CONNECT_TIMEOUT);
			} catch (IOException e) {
				//close() --> here
				e.printStackTrace();
			}
		}
		else{
			try {
				serverSocket = new ServerSocket(nPort);
				socket = serverSocket.accept();
				serverSocket.close();
				serverSocket = null;
			} catch (IOException e) {
				//close() --> here
				e.printStackTrace();
			}			
		}
	}


	public  final class ThreadConnect extends Thread{
		public void run() {
			bConnected = false;
			try {
				getSocket();
				if(socket==null) {
					// socketServer.close() --> here
					sendResetMsg(MSG_ConnectError);
					return;
				}

				if(nDataSort == DataSort.BYTES){
					bis = new BufferedInputStream(socket.getInputStream());
					bos = new BufferedOutputStream(socket.getOutputStream());
				}
				else{
					inReader = new BufferedReader(new InputStreamReader(
							socket.getInputStream()));
					outWriter = new BufferedWriter(new OutputStreamWriter(
							socket.getOutputStream()));
				}
				bConnected = true;
			} catch (UnknownHostException e) {
				e.printStackTrace();
			} catch (IOException e) {
				//connect timeout 07/25/15
				e.printStackTrace(); 
			}

			if (bConnected){
				mHandler.obtainMessage(MSG_Connected, 
						0, 0,null).sendToTarget();
				threadRead = new ThreadRead();
				threadRead.start();
			}
			else{
				// socket.close() --> here
				sendResetMsg(MSG_ConnectError);
			}
		}
	}


	public  final class ThreadRead extends Thread{
		boolean bDataArrived;
		public void run() {
			while(true){
				if(bConnected != true) return;
				if(bStopConnect) return;
				try {
					bDataArrived = false;
					if(nDataSort==DataSort.BYTES){
						if(bis.available()>0) bDataArrived = true;
					}
					else{
						if(inReader.ready()) bDataArrived = true;						
					}
					if(bDataArrived){
						Object oData = readData();
						if(bConnected != true) return; //be necessary!!
						mHandler.obtainMessage(MSG_DataArrived, 0, 0, 
								oData).sendToTarget();
					}
				} catch (IOException e) {
					e.printStackTrace();
					sendResetMsg(MSG_ReadError);
				}
			}
		}
	}


	public TcpClientServer(Handler mHandler, String strServer, int nPort ) {
		this.mHandler = mHandler;
		this.strServer = strServer;
		this.nPort = nPort;
		nWorkMode = WorkMode.CLIENT;
	}


	public TcpClientServer(Handler mHandler, int nPort) {
		this.mHandler = mHandler;
		this.nPort = nPort;
		nWorkMode = WorkMode.SERVER;
	}


	public String getRemoteIP(){
		if(socket==null) return "";		
		InetAddress ia = socket.getInetAddress();
		byte[] ipArray = ia.getAddress();
		return String.format(Locale.US, "%d.%d.%d.%d", 
				((int)ipArray[0])&0xff, ((int)ipArray[1])&0xff,
				((int)ipArray[2])&0xff, ((int)ipArray[3])&0xff);
	}


	public synchronized void setDataSort(DataSort nDataSort){
		if(bConnected) return;
		this.nDataSort = nDataSort;
	}


	public synchronized void setReadDelay(int nDelay){
		if((nDelay<DEFAULT_MIN_DELAY)||
				(nDelay>DEFAULT_MAX_DELAY))
			this.nDelay = DEFAULT_MIN_DELAY;
		else
			this.nDelay = nDelay;
	}


	public void connect(){
		threadConnect = new ThreadConnect();
		threadConnect.start();
	}


	public void listen(){
		connect();
	}


	private Object readData(){
		if(nDataSort == DataSort.BYTES)
			return readByteBuffer();
		else
			return readText();
	}


	private synchronized void sendResetMsg(int msg){
		bConnected = false;
		bStopConnect = true;
		mHandler.obtainMessage(msg, 
				0, 0, null).sendToTarget();
	}


	private String readText(){
		StringBuffer sb = new StringBuffer();
		String strData="";
		long lStart = System.currentTimeMillis();

		while(System.currentTimeMillis()-lStart < nDelay){
			try{
				while(inReader.ready()){ 
					sb.append((char)(inReader.read()));
					strData = sb.toString();
					if(isValidOfTextEnd(strData)) return strData;
					lStart = System.currentTimeMillis();
				}
			}catch (IOException e) {
				e.printStackTrace();
				sendResetMsg(MSG_ReadError);
			}			
		}
		return strData;
	}


	private boolean isValidOfTextEnd(String strData){
		switch(nDataSort){
		case TEXT_CR:
			if(strData.startsWith("Interval?!\r")){
				sendText("OK!"+"\r");
				mHandler.obtainMessage(MSG_ReplyOK, 
						0, 0,null).sendToTarget();							
			}
			if(strData.endsWith("\r")) return true;
			break;
		case TEXT_CRLF:
			if(strData.startsWith("Interval?!\r\n")){
				sendText("OK!"+"\r\n");
				mHandler.obtainMessage(MSG_ReplyOK, 
						0, 0,null).sendToTarget();							
			}
			if(strData.endsWith("\r\n")) return true;
			break;
		default:
			break;
		}
		return false;		
	}


	private ByteBuffer readByteBuffer(){
		ByteBuffer buf = ByteBuffer.allocate(1024);
		long lStart = System.currentTimeMillis();

		while(System.currentTimeMillis()-lStart < nDelay){
			try{
				while(bis.available()>0){
					byte[] bIn = new byte[bis.available()];
					bis.read(bIn, 0, bis.available());
					buf.put(bIn);
					lStart = System.currentTimeMillis();
				}
			}catch (IOException e) {
				e.printStackTrace();
				sendResetMsg(MSG_ReadError);
			}			
		}
		return buf;
	}


	public void sendText(String strText){
		try {
			outWriter.write(strText);
			outWriter.flush();
		} catch (IOException e) {
			e.printStackTrace();
			sendResetMsg(MSG_TransmitError);
		}
	}


	public void sendBytes(byte[] bOut){
		try {
			bos.write(bOut, 0, bOut.length);
			bos.flush();
		} catch (IOException e) {
			e.printStackTrace();
			sendResetMsg(MSG_TransmitError);
		}
	}


	public synchronized void close(){
		sendResetMsg(MSG_Closed);
		try {
			if(nDataSort == DataSort.BYTES){
				if(bis!=null) bis.close();
				if(bos!=null) bos.close();
			}
			else{
				if(inReader!=null) inReader.close();
				if(outWriter!=null) outWriter.close();
			}
			if(socket!=null) 
				if(!socket.isClosed()) socket.close();
			if(serverSocket!=null)
				if(!serverSocket.isClosed()) serverSocket.close();

		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}